﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using FitNesseRunner.Model;

namespace FitNesseRunner
{
	internal class DecisionTable : ExecutableTable
	{
		private readonly IReporter _reporter;
		private readonly List<string> _columnNames;

		public DecisionTable(IEnumerable<IEnumerable<string>> cells, IReporter reporter, IReflectionProvider reflectionProvider)
			: base(cells, 0, reflectionProvider)
		{
			_reporter = reporter;
			_columnNames = cells.ElementAt(1).Select(ToPascalCase).ToList();
		}

		public override void ExecuteAll()
		{
			object instance;
			try
			{
				instance = CreateInstance();
			}
			catch (Exception e)
			{
				_reporter.FailedToConstruct(ClassName, e);
				return;
			}

			var executeMethod = instance.GetType().GetMethod("Execute", new Type[0]);

			var valueLines = Cells.Skip(2);
			foreach (var line in valueLines)
			{
				var i = 0;
				foreach (var cell in line)
				{
					var value = cell.Trim();
					if (i >= _columnNames.Count())
						break;

					var columnName = _columnNames[i].Trim();
					if (columnName.EndsWith("?"))
					{
						var propertyInfo = GetPropertyInfo(instance, columnName.TrimEnd('?'));
						VerifyProperty(instance, propertyInfo, value);
					}
					else
					{
						var propertyInfo = GetPropertyInfo(instance, columnName);
						SetProperty(instance, propertyInfo, value);
					}
					i++;
				}

				if (executeMethod == null) 
					continue;

				executeMethod.Invoke(instance, new object[0]);
				_reporter.Invoking(instance.GetType().Name, "Execute", Enumerable.Empty<string>());
			}
		}

		private void VerifyProperty(object instance, PropertyInfo propertyInfo, string expectedValueString)
		{
			object actualValue;
			try
			{
				actualValue = GetProperty(propertyInfo, instance);
			}
			catch (Exception ex)
			{
				_reporter.InvocationFailed(instance.GetType().Name, propertyInfo.Name, Enumerable.Empty<string>(), ex);
				return;
			}

			if (expectedValueString == string.Empty)
			{
				_reporter.Show(propertyInfo.Name, actualValue.ToString());
				return;
			}

			var propertyType = propertyInfo.PropertyType;
			var expectedValue = Convert.ChangeType(expectedValueString, propertyType);
			if (actualValue.Equals(expectedValue))
			{
				_reporter.Pass("{0}.{1}=={2}", instance.GetType().Name, propertyInfo.Name, expectedValue);
			}
			else
			{
				_reporter.Failed(instance.GetType().Name, propertyInfo.Name, expectedValue, actualValue);
			}
		}

		private static object GetProperty(PropertyInfo propertyInfo, object instance)
		{
			var getter = propertyInfo.GetGetMethod();
			return getter.Invoke(instance, new object[0]);
		}

		private void SetProperty(object instance, PropertyInfo propertyInfo, string stringValue)
		{
			var setter = propertyInfo.GetSetMethod(true);
			var value = GetValueForAssignment(propertyInfo, stringValue);
			setter.Invoke(instance, new[] {value});
			_reporter.Invoking(propertyInfo, stringValue);
		}

		private static object GetValueForAssignment(PropertyInfo propertyInfo, string stringValue)
		{
			var propertyType = propertyInfo.PropertyType;
			if (IsNullable(propertyType))
			{
				if (stringValue.Trim() == string.Empty)
					return null;

				propertyType = propertyType.GetGenericArguments().Single();
			}
			
			return Convert.ChangeType(stringValue, propertyType);
		}

		private static bool IsNullable(Type propertyType)
		{
			return propertyType.IsGenericType && propertyType.GetGenericTypeDefinition() == typeof (Nullable<>);
		}

		private static PropertyInfo GetPropertyInfo(object instance, string propertyName)
		{
			var type = instance.GetType();
			return type.GetProperty(propertyName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.IgnoreCase);
		}
	}
}